package worker

import (
	"context"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"strings"
	"time"

	"github.com/fleetdm/fleet/v4/pkg/fleetdbase"
	"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
	"github.com/fleetdm/fleet/v4/server/contexts/license"
	"github.com/fleetdm/fleet/v4/server/fleet"
	apple_mdm "github.com/fleetdm/fleet/v4/server/mdm/apple"
	"github.com/fleetdm/fleet/v4/server/mdm/apple/appmanifest"
	"github.com/fleetdm/fleet/v4/server/ptr"
	kitlog "github.com/go-kit/log"
	"github.com/go-kit/log/level"
	"github.com/google/uuid"
)

// Name of the Apple MDM job as registered in the worker. Note that although it
// is a single job, it can process a number of different-but-related tasks,
// identified by the Task field in the job's payload.
const appleMDMJobName = "apple_mdm"

type AppleMDMTask string

// List of supported tasks.
const (
	AppleMDMPostDEPEnrollmentTask    AppleMDMTask = "post_dep_enrollment"
	AppleMDMPostManualEnrollmentTask AppleMDMTask = "post_manual_enrollment"
	// PostDEPReleaseDevice is not enqueued anymore for macOS but remains for
	// backward compatibility (processing existing jobs after a fleet upgrade)
	// and for ios/ipados. Macs are now released via the swift dialog UI of the
	// setup experience flow.
	AppleMDMPostDEPReleaseDeviceTask AppleMDMTask = "post_dep_release_device"
)

// AppleMDM is the job processor for the apple_mdm job.
type AppleMDM struct {
	Datastore             fleet.Datastore
	Log                   kitlog.Logger
	Commander             *apple_mdm.MDMAppleCommander
	BootstrapPackageStore fleet.MDMBootstrapPackageStore
	VPPInstaller          fleet.AppleMDMVPPInstaller
}

// Name returns the name of the job.
func (a *AppleMDM) Name() string {
	return appleMDMJobName
}

// appleMDMArgs is the payload for the Apple MDM job.
type appleMDMArgs struct {
	Task     AppleMDMTask `json:"task"`
	HostUUID string       `json:"host_uuid"`
	TeamID   *uint        `json:"team_id,omitempty"`
	// EnrollReference is the UUID of the MDM IdP account used to enroll the
	// device. It is used to set the username and full name of the user
	// associated with the device.
	//
	// FIXME: Rename this to IdPAccountUUID or something similar.
	EnrollReference        string     `json:"enroll_reference,omitempty"`
	EnrollmentCommands     []string   `json:"enrollment_commands,omitempty"`
	Platform               string     `json:"platform,omitempty"`
	UseWorkerDeviceRelease bool       `json:"use_worker_device_release,omitempty"`
	ReleaseDeviceAttempt   int        `json:"release_device_attempt,omitempty"`    // number of attempts to release the device
	ReleaseDeviceStartedAt *time.Time `json:"release_device_started_at,omitempty"` // time when the release device task first started
}

// Run executes the apple_mdm job.
func (a *AppleMDM) Run(ctx context.Context, argsJSON json.RawMessage) error {
	appCfg, err := a.Datastore.AppConfig(ctx)
	if err != nil {
		return ctxerr.Wrap(ctx, err, "retrieving app config")
	}
	if !appCfg.MDM.EnabledAndConfigured || a.Commander == nil {
		return nil
	}

	var args appleMDMArgs
	if err := json.Unmarshal(argsJSON, &args); err != nil {
		return ctxerr.Wrap(ctx, err, "unmarshal args")
	}

	switch args.Task {
	case AppleMDMPostDEPEnrollmentTask:
		err := a.runPostDEPEnrollment(ctx, args)
		return ctxerr.Wrap(ctx, err, "running post Apple DEP enrollment task")

	case AppleMDMPostManualEnrollmentTask:
		err := a.runPostManualEnrollment(ctx, args)
		return ctxerr.Wrap(ctx, err, "running post Apple manual enrollment task")

	case AppleMDMPostDEPReleaseDeviceTask:
		err := a.runPostDEPReleaseDevice(ctx, args)
		return ctxerr.Wrap(ctx, err, "running post Apple DEP release device task")

	default:
		return ctxerr.Errorf(ctx, "unknown task: %v", args.Task)
	}
}

func isMacOS(platform string) bool {
	// For backwards compatibility, we assume empty platform in job arguments is macOS.
	return platform == "" ||
		platform == "darwin"
}

func (a *AppleMDM) runPostManualEnrollment(ctx context.Context, args appleMDMArgs) error {
	if isMacOS(args.Platform) {
		if _, err := a.installFleetd(ctx, args.HostUUID); err != nil {
			return ctxerr.Wrap(ctx, err, "installing post-enrollment packages")
		}
	} else {
		// We shouldn't have any setup experience steps if we're not on a premium license,
		// but best to check anyway plus it saves some db queries.
		if license.IsPremium(ctx) {
			_, err := a.installSetupExperienceVPPAppsOnIosIpadOS(ctx, args.HostUUID)
			if err != nil {
				return ctxerr.Wrap(ctx, err, "installing setup experience VPP apps on iOS/iPadOS")
			}
		}
	}

	return nil
}

func (a *AppleMDM) runPostDEPEnrollment(ctx context.Context, args appleMDMArgs) error {
	var (
		awaitCmdUUIDs []string
		appCfg        *fleet.AppConfig
		team          *fleet.Team
		err           error
	)

	if isMacOS(args.Platform) {
		var manualAgentInstall bool
		if args.TeamID == nil {
			if appCfg, err = a.getAppConfig(ctx, appCfg); err != nil {
				return err
			}
			manualAgentInstall = appCfg.MDM.MacOSSetup.ManualAgentInstall.Value
		} else {
			if team, err = a.getTeamConfig(ctx, team, *args.TeamID); err != nil {
				return err
			}
			manualAgentInstall = team.Config.MDM.MacOSSetup.ManualAgentInstall.Value
		}

		if !manualAgentInstall {
			fleetdCmdUUID, err := a.installFleetd(ctx, args.HostUUID)
			if err != nil {
				return ctxerr.Wrap(ctx, err, "installing post-enrollment packages")
			}
			awaitCmdUUIDs = append(awaitCmdUUIDs, fleetdCmdUUID)
		}

		bootstrapCmdUUID, err := a.installBootstrapPackage(ctx, args.HostUUID, args.TeamID)
		if err != nil {
			return ctxerr.Wrap(ctx, err, "installing post-enrollment packages")
		}
		if bootstrapCmdUUID != "" {
			awaitCmdUUIDs = append(awaitCmdUUIDs, bootstrapCmdUUID)
		}
	} else {
		// TODO: We likely want to wait for the actual installs to complete not just the commands to be ack'd
		commandUUIDs, err := a.installSetupExperienceVPPAppsOnIosIpadOS(ctx, args.HostUUID)
		if err != nil {
			return ctxerr.Wrap(ctx, err, "installing setup experience VPP apps on iOS/iPadOS")
		}
		awaitCmdUUIDs = append(awaitCmdUUIDs, commandUUIDs...)
	}

	if ref := args.EnrollReference; ref != "" {
		a.Log.Log("info", "got an enroll_reference", "host_uuid", args.HostUUID, "ref", ref)
		if appCfg, err = a.getAppConfig(ctx, appCfg); err != nil {
			return err
		}

		acct, err := a.Datastore.GetMDMIdPAccountByUUID(ctx, ref)
		if err != nil {
			return ctxerr.Wrapf(ctx, err, "getting idp account details for enroll reference %s", ref)
		}

		ssoEnabled := appCfg.MDM.MacOSSetup.EnableEndUserAuthentication
		if args.TeamID != nil {
			if team, err = a.getTeamConfig(ctx, team, *args.TeamID); err != nil {
				return err
			}
			ssoEnabled = team.Config.MDM.MacOSSetup.EnableEndUserAuthentication
		}

		if ssoEnabled {
			fullName, err := a.getIdPDisplayName(ctx, acct, args)
			if err != nil {
				return ctxerr.Wrap(ctx, err, "getting idp account display name")
			}
			a.Log.Log("info", "setting username and fullname", "host_uuid", args.HostUUID)
			cmdUUID := uuid.New().String()
			if err := a.Commander.AccountConfiguration(
				ctx,
				[]string{args.HostUUID},
				cmdUUID,
				fullName,
				acct.Username,
			); err != nil {
				return ctxerr.Wrap(ctx, err, "sending AccountConfiguration command")
			}
			awaitCmdUUIDs = append(awaitCmdUUIDs, cmdUUID)
		}
	}

	// proceed to release the device if it is not a macos, as those are released
	// via the setup experience flow, or if we were told to use the worker based
	// release.
	if !isMacOS(args.Platform) || args.UseWorkerDeviceRelease {
		var manualRelease bool
		if args.TeamID == nil {
			if appCfg, err = a.getAppConfig(ctx, appCfg); err != nil {
				return err
			}
			manualRelease = appCfg.MDM.MacOSSetup.EnableReleaseDeviceManually.Value
		} else {
			if team, err = a.getTeamConfig(ctx, team, *args.TeamID); err != nil {
				return err
			}
			manualRelease = team.Config.MDM.MacOSSetup.EnableReleaseDeviceManually.Value
		}

		if !manualRelease {
			// send all command uuids for the commands sent here during post-DEP
			// enrollment and enqueue a job to look for the status of those commands to
			// be final and same for MDM profiles of that host; it means the DEP
			// enrollment process is done and the device can be released.
			if err := QueueAppleMDMJob(ctx, a.Datastore, a.Log, AppleMDMPostDEPReleaseDeviceTask,
				args.HostUUID, args.Platform, args.TeamID, args.EnrollReference, false, awaitCmdUUIDs...); err != nil {
				return ctxerr.Wrap(ctx, err, "queue Apple Post-DEP release device job")
			}
		}
	}

	return nil
}

// getTeamConfig gets team config from DB if not provided.
func (a *AppleMDM) getTeamConfig(ctx context.Context, team *fleet.Team, teamID uint) (*fleet.Team, error) {
	if team == nil { // TODO see if we can swap this (plus callers) to use TeamLite
		var err error
		team, err = a.Datastore.TeamWithExtras(ctx, teamID)
		if err != nil {
			return nil, ctxerr.Wrap(ctx, err, "fetch team to send AccountConfiguration")
		}
	}
	return team, nil
}

// getAppConfig gets app config from DB if not provided.
func (a *AppleMDM) getAppConfig(ctx context.Context, appConfig *fleet.AppConfig) (*fleet.AppConfig, error) {
	if appConfig == nil {
		var err error
		appConfig, err = a.Datastore.AppConfig(ctx)
		if err != nil {
			return nil, ctxerr.Wrap(ctx, err, "getting app config")
		}
	}
	return appConfig, nil
}

func (a *AppleMDM) getIdPDisplayName(ctx context.Context, acct *fleet.MDMIdPAccount, args appleMDMArgs) (string, error) {
	if acct.Fullname != "" && !strings.Contains(acct.Fullname, "@") {
		return acct.Fullname, nil
	}

	// If full name is empty or appears to be an email, see if it exists via SCIM integration
	scimUser, err := a.Datastore.ScimUserByUserNameOrEmail(ctx, acct.Username, acct.Email)
	switch {
	case err != nil && !fleet.IsNotFound(err):
		return "", ctxerr.Wrap(ctx, err, "getting scim user details for enroll reference %s and host_uuid %s", acct.UUID, args.HostUUID)
	case scimUser == nil:
		return acct.Fullname, nil
	}
	if scimUser.DisplayName() == "" {
		return acct.Fullname, nil
	}
	return scimUser.DisplayName(), nil
}

// This job is used only for iDevices or for macos devices that don't use any
// setup experience items (software installs, script exec) - see
// appleMDMArgs.UseWorkerDeviceRelease. Otherwise releasing devices is now done
// via the orbit endpoint /setup_experience/status that is polled by a swift
// dialog UI window during the setup process, and automatically releases the
// device once all pending setup tasks are done.
func (a *AppleMDM) runPostDEPReleaseDevice(ctx context.Context, args appleMDMArgs) error {
	// Edge cases:
	//   - if the device goes offline for a long time, should we go ahead and
	//   release after a while?
	//   - if some commands/profiles failed (a final state), should we go ahead
	//   and release?
	//   - if the device keeps moving team, or profiles keep being added/removed
	//   from its team, it's possible that its profiles will never settle and
	//   always have pending statuses. Same as going offline, should we release
	//   after a while?
	//
	// We opted "yes" to all those, and we want to release after a few minutes,
	// not hours, so we'll allow only a couple retries.

	const (
		maxWaitTime         = 15 * time.Minute
		minAttempts         = 10
		maxAttempts         = 30
		nextAttemptMinDelay = 30 * time.Second
	)

	args.ReleaseDeviceAttempt++
	if args.ReleaseDeviceStartedAt == nil {
		now := time.Now().UTC()
		args.ReleaseDeviceStartedAt = &now
	}

	level.Debug(a.Log).Log(
		"task", "runPostDEPReleaseDevice",
		"msg", fmt.Sprintf("awaiting commands %v and profiles to settle for host %s", args.EnrollmentCommands, args.HostUUID),
		"attempt", args.ReleaseDeviceAttempt,
		"started_at", args.ReleaseDeviceStartedAt.Format(time.RFC3339),
	)

	// if we've reached the minimum number of attempts and the maximum time to
	// wait, we release the device even if some commands or profiles are still
	// pending. We also release in case it reached the maximum number of
	// attempts, to prevent an issue with clock skew where the wait delay does
	// not appear to be reached.
	if (args.ReleaseDeviceAttempt >= minAttempts && time.Since(*args.ReleaseDeviceStartedAt) >= maxWaitTime) ||
		(args.ReleaseDeviceAttempt >= maxAttempts) {
		a.Log.Log("info", "releasing device after too many attempts or too long wait", "host_uuid", args.HostUUID, "attempts", args.ReleaseDeviceAttempt)
		if err := a.Commander.DeviceConfigured(ctx, args.HostUUID, uuid.NewString()); err != nil {
			return ctxerr.Wrapf(ctx, err, "failed to enqueue DeviceConfigured command after %d attempts", args.ReleaseDeviceAttempt)
		}
		return nil
	}

	reenqueueTask := func() error {
		// re-enqueue the same job, but now
		// ReleaseDeviceAttempt/ReleaseDeviceStartedAt have been incremented/set,
		// and run it not before a delay so it doesn't run again until the next
		// worker cycle.
		_, err := QueueJobWithDelay(ctx, a.Datastore, appleMDMJobName, args, nextAttemptMinDelay)
		return err
	}

	for _, cmdUUID := range args.EnrollmentCommands {
		if cmdUUID == "" {
			continue
		}

		res, err := a.Datastore.GetMDMAppleCommandResults(ctx, cmdUUID)
		if err != nil {
			return ctxerr.Wrap(ctx, err, "failed to get MDM command results")
		}

		var completed bool
		for _, r := range res {
			// succeeded or failed, it is done (final state)
			if r.Status == fleet.MDMAppleStatusAcknowledged || r.Status == fleet.MDMAppleStatusError ||
				r.Status == fleet.MDMAppleStatusCommandFormatError {
				completed = true
				break
			}
		}

		if !completed {
			// DEP enrollment commands are not done being delivered to that device,
			// cannot release it now.
			if err := reenqueueTask(); err != nil {
				return fmt.Errorf("failed to re-enqueue task: %w", err)
			}
			return nil
		}
		level.Debug(a.Log).Log(
			"task", "runPostDEPReleaseDevice",
			"msg", fmt.Sprintf("command %s has completed", cmdUUID),
		)
	}

	// all DEP-enrollment commands are done, check the host's profiles
	profs, err := a.Datastore.GetHostMDMAppleProfiles(ctx, args.HostUUID)
	if err != nil {
		return ctxerr.Wrap(ctx, err, "failed to get host MDM profiles")
	}
	for _, prof := range profs {
		// NOTE: DDM profiles (declarations) are ignored because while a device is
		// awaiting to be released, it cannot process a DDM session (at least
		// that's what we noticed during testing).
		if strings.HasPrefix(prof.ProfileUUID, fleet.MDMAppleDeclarationUUIDPrefix) {
			continue
		}

		// NOTE: user-scoped profiles are ignored because they are not sent by Fleet
		// until after the device is released - there is no user-channel available
		// on the host until after the release, and after the user actually created
		// the user account.
		if prof.Scope == fleet.PayloadScopeUser {
			continue
		}

		// if it has any pending profiles, then its profiles are not done being
		// delivered (installed or removed).
		if prof.Status == nil || *prof.Status == fleet.MDMDeliveryPending {
			if err := reenqueueTask(); err != nil {
				return fmt.Errorf("failed to re-enqueue task: %w", err)
			}
			return nil
		}
		level.Debug(a.Log).Log(
			"task", "runPostDEPReleaseDevice",
			"msg", fmt.Sprintf("profile %s has been deployed", prof.Identifier),
		)
	}

	profilesMissingInstallation, err := a.Datastore.ListMDMAppleProfilesToInstall(ctx, args.HostUUID) // Get profiles that are missing to be installed on this host
	if err != nil {
		return ctxerr.Wrap(ctx, err, "failed to list profiles missing installation")
	}
	profilesMissingInstallation = fleet.FilterOutUserScopedProfiles(profilesMissingInstallation)
	if !isMacOS(args.Platform) {
		profilesMissingInstallation = fleet.FilterMacOSOnlyProfilesFromIOSIPadOS(profilesMissingInstallation)
	}

	if len(profilesMissingInstallation) > 0 {
		level.Info(a.Log).Log("msg", "re-enqueuing due to profiles missing installation", "host_uuid", args.HostUUID)
		// requeue the task if some profiles are still missing.
		if err := reenqueueTask(); err != nil {
			return ctxerr.Wrap(ctx, err, "failed to re-enqueue task")
		}
		return nil
	}

	if !isMacOS(args.Platform) {
		setupExperienceStatuses, err := a.Datastore.ListSetupExperienceResultsByHostUUID(ctx, args.HostUUID)
		if err != nil {
			return ctxerr.Wrap(ctx, err, "retrieving setup experience status results for host pending DEP release")
		}
		for _, status := range setupExperienceStatuses {
			if status.Status == fleet.SetupExperienceStatusPending || status.Status == fleet.SetupExperienceStatusRunning {
				level.Info(a.Log).Log("msg", "re-enqueuing due to setup experience items still pending or running", "host_uuid", args.HostUUID, "status_id", status.ID)
				if err := reenqueueTask(); err != nil {
					return ctxerr.Wrap(ctx, err, "failed to re-enqueue task due to pending setup experience items")
				}
				return nil
			}
		}
	}

	// release the device
	a.Log.Log("info", "releasing device, all DEP enrollment commands and profiles have completed", "host_uuid", args.HostUUID)
	if err := a.Commander.DeviceConfigured(ctx, args.HostUUID, uuid.NewString()); err != nil {
		return ctxerr.Wrap(ctx, err, "failed to enqueue DeviceConfigured command")
	}
	return nil
}

func (a *AppleMDM) installFleetd(ctx context.Context, hostUUID string) (string, error) {
	manifestURL := fleetdbase.GetPKGManifestURL()
	cmdUUID := uuid.New().String()
	if err := a.Commander.InstallEnterpriseApplication(ctx, []string{hostUUID}, cmdUUID, manifestURL); err != nil {
		return "", err
	}
	a.Log.Log("info", "sent command to install fleetd", "host_uuid", hostUUID)
	return cmdUUID, nil
}

func (a *AppleMDM) installSetupExperienceVPPAppsOnIosIpadOS(ctx context.Context, hostUUID string) ([]string, error) {
	statuses, err := a.Datastore.ListSetupExperienceResultsByHostUUID(ctx, hostUUID)
	if err != nil {
		return nil, ctxerr.Wrap(ctx, err, "retrieving setup experience status results for next step")
	}

	var appsPending []*fleet.SetupExperienceStatusResult
	commandUUIDs := []string{}
	for _, status := range statuses {
		if err := status.IsValid(); err != nil {
			return nil, ctxerr.Wrap(ctx, err, "invalid row")
		}

		switch {
		case status.VPPAppTeamID != nil:
			if status.Status == fleet.SetupExperienceStatusPending {
				appsPending = append(appsPending, status)
			}
		case status.SetupExperienceScriptID != nil, status.SoftwareInstallerID != nil:
			status.Status = fleet.SetupExperienceStatusFailure
			err = a.Datastore.UpdateSetupExperienceStatusResult(ctx, status)
			if err != nil {
				return nil, ctxerr.Wrap(ctx, err, "updating setup experience status result to failure")
			}
			// If we enqueued a non-VPP item for an iOS/iPadOS device, it likely a code bug
			level.Error(a.Log).Log("msg", "unexpected setup experience item for iOS/iPadOS device, only VPP apps are supported", "host_uuid", hostUUID, "status_id", status.ID)
		}
	}

	if len(appsPending) > 0 {
		// enqueue vpp apps
		// TODO Is there a better way to get a host by UUID? This is a somewhat "wide" search which feels unnecessary
		host, err := a.Datastore.HostByIdentifier(ctx, hostUUID)
		if err != nil {
			return nil, ctxerr.Wrap(ctx, err, "retrieving host by UUID")
		}
		for _, app := range appsPending {
			vppAppID, err := app.VPPAppID()
			if err != nil {
				return nil, ctxerr.Wrap(ctx, err, "constructing vpp app details for installation")
			}

			if app.SoftwareTitleID == nil {
				return nil, ctxerr.Errorf(ctx, "setup experience software title id missing from vpp app install request: %d", app.ID)
			}

			vppApp := &fleet.VPPApp{
				TitleID: *app.SoftwareTitleID,
				VPPAppTeam: fleet.VPPAppTeam{
					VPPAppID: *vppAppID,
				},
			}

			opts := fleet.HostSoftwareInstallOptions{
				SelfService:        false,
				ForSetupExperience: true,
			}

			cmdUUID, err := a.installSoftwareFromVPP(ctx, host, vppApp, true, opts)

			app.NanoCommandUUID = &cmdUUID
			app.Status = fleet.SetupExperienceStatusRunning

			if err != nil {
				// if we get an error (e.g. no available licenses) while attempting to enqueue the
				// install, then we should immediately go to an error state so setup experience
				// isn't blocked.
				level.Error(a.Log).Log("msg", "got an error when attempting to enqueue VPP app install", "err", err, "adam_id", app.VPPAppAdamID)
				app.Status = fleet.SetupExperienceStatusFailure
				app.Error = ptr.String(err.Error())
			} else {
				commandUUIDs = append(commandUUIDs, cmdUUID)
			}
			if err := a.Datastore.UpdateSetupExperienceStatusResult(ctx, app); err != nil {
				return nil, ctxerr.Wrap(ctx, err, "updating setup experience with vpp install command uuid")
			}
		}
	}

	return commandUUIDs, nil
}

func (a *AppleMDM) installSoftwareFromVPP(ctx context.Context, host *fleet.Host, vppApp *fleet.VPPApp, appleDevice bool, opts fleet.HostSoftwareInstallOptions) (string, error) {
	// Should not happen in the normal course of events but can happen in tests
	// and likely indicates things weren't initialized properly.
	if a.VPPInstaller == nil {
		return "", errors.New("VPP installer not configured")
	}
	token, err := a.VPPInstaller.GetVPPTokenIfCanInstallVPPApps(ctx, appleDevice, host)
	if err != nil {
		return "", err
	}

	return a.VPPInstaller.InstallVPPAppPostValidation(ctx, host, vppApp, token, opts)
}

func (a *AppleMDM) installBootstrapPackage(ctx context.Context, hostUUID string, teamID *uint) (string, error) {
	// GetMDMAppleBootstrapPackageMeta expects team id 0 for no team
	var tmID uint
	if teamID != nil {
		tmID = *teamID
	}
	meta, err := a.Datastore.GetMDMAppleBootstrapPackageMeta(ctx, tmID)
	if err != nil {
		var nfe fleet.NotFoundError
		if errors.As(err, &nfe) {
			a.Log.Log("info", "unable to find a bootstrap package for DEP enrolled device, skipping installation", "host_uuid", hostUUID)
			return "", nil
		}

		return "", err
	}

	// Get CloudFront CDN signed URL if configured
	url := a.getSignedURL(ctx, meta)

	if url == "" {
		appCfg, err := a.Datastore.AppConfig(ctx)
		if err != nil {
			return "", err
		}

		url, err = meta.URL(appCfg.MDMUrl())
		if err != nil {
			return "", err
		}
	}

	manifest := appmanifest.NewFromSha(meta.Sha256, url)
	cmdUUID := uuid.New().String()
	err = a.Commander.InstallEnterpriseApplicationWithEmbeddedManifest(ctx, []string{hostUUID}, cmdUUID, manifest)
	if err != nil {
		return "", err
	}
	err = a.Datastore.RecordHostBootstrapPackage(ctx, cmdUUID, hostUUID)
	if err != nil {
		return "", err
	}
	a.Log.Log("info", "sent command to install bootstrap package", "host_uuid", hostUUID)
	return cmdUUID, nil
}

func (a *AppleMDM) getSignedURL(ctx context.Context, meta *fleet.MDMAppleBootstrapPackage) string {
	var url string
	if a.BootstrapPackageStore != nil {
		pkgID := hex.EncodeToString(meta.Sha256)
		signedURL, err := a.BootstrapPackageStore.Sign(ctx, pkgID)
		switch {
		case errors.Is(err, fleet.ErrNotConfigured):
			// no CDN configured, fall back to the MDM URL
		case err != nil:
			// log the error but continue with the MDM URL
			level.Error(a.Log).Log("msg", "failed to sign bootstrap package URL", "err", err)
		default:
			exists, err := a.BootstrapPackageStore.Exists(ctx, pkgID)
			switch {
			case err != nil:
				// log the error but continue with the MDM URL
				level.Error(a.Log).Log("msg", "failed to check if bootstrap package exists", "err", err)
			case !exists:
				// log the error but continue with the MDM URL
				level.Error(a.Log).Log("msg", "bootstrap package does not exist in package store", "pkg_id", pkgID)
			default:
				url = signedURL
			}
		}
	}
	return url
}

// QueueAppleMDMJob queues a apple_mdm job for one of the supported tasks, to
// be processed asynchronously via the worker.
func QueueAppleMDMJob(
	ctx context.Context,
	ds fleet.Datastore,
	logger kitlog.Logger,
	task AppleMDMTask,
	hostUUID string,
	platform string,
	teamID *uint,
	enrollReference string,
	useWorkerDeviceRelease bool,
	enrollmentCommandUUIDs ...string,
) error {
	attrs := []interface{}{
		"enabled", "true",
		appleMDMJobName, task,
		"host_uuid", hostUUID,
		"platform", platform,
		"with_enroll_reference", enrollReference != "",
	}
	if teamID != nil {
		attrs = append(attrs, "team_id", *teamID)
	}
	if len(enrollmentCommandUUIDs) > 0 {
		attrs = append(attrs, "enrollment_commands", fmt.Sprintf("%v", enrollmentCommandUUIDs))
	}
	level.Info(logger).Log(attrs...)

	args := &appleMDMArgs{
		Task:                   task,
		HostUUID:               hostUUID,
		TeamID:                 teamID,
		EnrollReference:        enrollReference,
		EnrollmentCommands:     enrollmentCommandUUIDs,
		Platform:               platform,
		UseWorkerDeviceRelease: useWorkerDeviceRelease,
	}

	// the release device task is always added with a delay
	var delay time.Duration
	if task == AppleMDMPostDEPReleaseDeviceTask {
		delay = 30 * time.Second
	}
	job, err := QueueJobWithDelay(ctx, ds, appleMDMJobName, args, delay)
	if err != nil {
		return ctxerr.Wrap(ctx, err, "queueing job")
	}
	level.Debug(logger).Log("job_id", job.ID)
	return nil
}
